package br.edu.ufcg.unknown.lexico;

import java.io.File;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import br.edu.ufcg.unknown.exception.AnalisadorLexicoUnknownRuntimeException;
import br.edu.ufcg.unknown.exception.UnknownException;
import br.edu.ufcg.unknown.exception.UnknownRuntimeException;
import br.edu.ufcg.unknown.sintatico.TabelaDeTokens;

/**
 * Analisador Léxico. Responsável por ler um fluxo de caracteres de entrada e devolver os tokens correspondentes.
 * 
 * @author Fábio Jorge
 * @author Jaindson Valentim
 * @author Moisés Rodrigues
 */
public class AnalisadorLexico {

	/**
	 * Pattern com os caracteres validos da linguagem utilizada por esse analisador léxico.
	 */
	private static Pattern patternCaracteresValidos = Pattern
			.compile("[ 0-9\r\t\n\fa-zA-Z_\\+\\-\\*/=:><\\)\\(\\[\\];']");

	/**
	 * Tamanho máximo do identificador.
	 */
	public static final int TAM_MAXIMO_IDENTIFICADOR = 50;

	/**
	 * Tamanho máximo para uma cadeia na linguagem do analisador.
	 */
	public static final int TAM_MAXIMO_CADEIA = 1024;

	/**
	 * Entidade que gerencia o fluxo de caracteres vindos do código fonte.
	 */
	private FluxoDeCaractere fluxoDeCaractere;

	private TabelaDeTokens tabelaDeTokens;

	/**
	 * Visibilidade <i>protected</i> para que seja possível testar métodos auxiliares nos testes de unidade.
	 */
	protected AnalisadorLexico() {

	}

	public AnalisadorLexico(String codigoFonte) throws UnknownException {
		this.fluxoDeCaractere = new FluxoDeCaractere(codigoFonte);
	}

	/**
	 * Cria um AnalisadorLéxico recebendo o caminho para o código fonte.
	 * 
	 * @param codigoFonte
	 *            caminho completo para o código fonte
	 * @throws UnknownException
	 */
	public AnalisadorLexico(String codigoFonte, File arquivoDePalavrasReservadas) throws UnknownException {
		this.fluxoDeCaractere = new FluxoDeCaractere(codigoFonte);
		this.tabelaDeTokens = new TabelaDeTokens(arquivoDePalavrasReservadas);
	}

	public AnalisadorLexico(File codigoFonte) throws UnknownException {
		this.fluxoDeCaractere = new FluxoDeCaractere(codigoFonte);
	}

	/**
	 * Cria um AnalisadorLéxico recebendo um objeto do tipo File que referencia o código fonte.
	 * 
	 * @param codigoFonte
	 *            objeto do tipo File que refencia o código fonte
	 * @throws UnknownException
	 */
	public AnalisadorLexico(File codigoFonte, File arquivoDePalavrasReservadas) throws UnknownException {
		this.fluxoDeCaractere = new FluxoDeCaractere(codigoFonte);
		this.tabelaDeTokens = new TabelaDeTokens(arquivoDePalavrasReservadas);
	}

	/**
	 * Compara um caracter recebido com os contidos no Patter de caracteres válidos para avaliar se o caracter
	 * recebido é ou não válido.
	 * 
	 * @param caractere
	 *            caracter a ser analisado
	 * @return true se o caracter for um dos válidos caso contrário false
	 */
	public boolean ehCaractereValido(Character caractere) {
		Matcher matcher = patternCaracteresValidos.matcher("" + caractere);
		return matcher.matches();
	}

	/**
	 * @return {@link FluxoDeCaractere#getLinha()}
	 */
	public int getLinha() {
		return this.fluxoDeCaractere.getLinha();
	}

	/**
	 * @return {@link FluxoDeCaractere#getColuna()}
	 */
	public int getColuna() {
		return this.fluxoDeCaractere.getColuna();
	}

	/**
	 * Retorna o próximo token lido do código fonte.
	 * 
	 * @return o próximo token lido do código fonte
	 */
	public Token getProxToken() {

		Token token = null;

		int linha = this.fluxoDeCaractere.getLinhaProxCaractereIgWS();
		int coluna = this.fluxoDeCaractere.getColunaProxCaractereIgWS();

		Character caractere = null;

		while (this.fluxoDeCaractere.temProxCaractereIgWS()
				&& !ehCaractereValido(caractere = this.fluxoDeCaractere.getProxCaractereIgWS())) {
			throw new AnalisadorLexicoUnknownRuntimeException(ErrosAnalisadorLexico.SIMBOLO_INVALIDO, caractere,
					this.fluxoDeCaractere.getLinha(), this.fluxoDeCaractere.getColuna());
		}

		if (caractere != null && ehCaractereValido(caractere)) {

			this.fluxoDeCaractere.devolver(caractere);

			if (ehInicioIdentificador(caractere)) {
				token = getIdentificador();
			} else if (ehInicioOperador(caractere)) {
				token = getOperador();
			} else if (ehInicioParentisador(caractere)) {
				token = getParentisador();
			} else if (ehInicioSinalDePontuacao(caractere)) {
				token = getSinalDePontuacao();
			} else if (ehInicioCadeia(caractere)) {
				token = getCadeia();
			} else if (ehInicioNumero(caractere)) {
				token = getNumero();
			} else {
				throw new UnknownRuntimeException(
						"FATAL! Erro inesperado! Lido símbolo inválido mas não foi reportado como tal", null);
			}

			token.setLinha(linha);
			token.setColuna(coluna);

			if (this.tabelaDeTokens != null) {
				this.tabelaDeTokens.resolverToken(token);
			}

			return token;
		}

		return null;

	}

	/**
	 * Observe que para o correto funcionamento o seguinte método deve ser chamado antes
	 * {@link #ehInicioNumero(java.lang.Character)}
	 * 
	 * @return O próximo número lido do fluxo de caracteres
	 */
	protected Token getNumero() {
		StringBuilder sb = new StringBuilder();
		Token token = null;
		Character caractere = null;

		while (this.fluxoDeCaractere.temProxCaractere()) {
			caractere = this.fluxoDeCaractere.getProxCaractere();

			if (!ehCaractereValido(caractere)) {
				throw new AnalisadorLexicoUnknownRuntimeException(ErrosAnalisadorLexico.SIMBOLO_INVALIDO,
						caractere, this.fluxoDeCaractere.getLinha(), this.fluxoDeCaractere.getColuna());
			}

			if (!Character.isDigit(caractere)) {
				break;
			}

			sb.append(caractere);
			caractere = null;
		}

		if (caractere != null) {
			this.fluxoDeCaractere.devolver(caractere);
		}

		String lexema = sb.toString();

		if (lexema.equals("")) {
			return null;
		}

		token = new Token(lexema, "t_numero");

		return token;
	}

	/**
	 * @param caractere
	 * @return True se for caractere início de Número
	 */
	protected boolean ehInicioNumero(Character caractere) {
		return Character.isDigit(caractere);
	}

	/**
	 * Uma cadeia de caracteres começa com o caractere "'", seguido por uma sequencia de símbolos válidos da
	 * Linguagem (exceto o próprio "'") e termina também com o símbolo "'". Uma cadeia de caracteres não pode se
	 * prolongar por mais de uma linha, isto caracteriza um erro. Observe que para o correto funcionamento o
	 * seguinte método deve ser chamado antes {@link AnalisadorLexico#ehInicioCadeia(Character)}
	 * 
	 * @return A próxima Cadeia lida do fluxo de caracteres
	 */
	protected Token getCadeia() {

		StringBuilder sb = new StringBuilder();
		Token token = null;
		Character caractere = null;

		int linha = this.fluxoDeCaractere.getLinhaProxCaractereIgWS();
		int coluna = this.fluxoDeCaractere.getColunaProxCaractereIgWS();

		if (this.fluxoDeCaractere.temProxCaractere()) {

			int contAspasSimples = 2;
			while (this.fluxoDeCaractere.temProxCaractere() && contAspasSimples > 0) {
				caractere = this.fluxoDeCaractere.getProxCaractere();

				if (!ehCaractereValido(caractere) && contAspasSimples == 2) {
					throw new AnalisadorLexicoUnknownRuntimeException(ErrosAnalisadorLexico.SIMBOLO_INVALIDO,
							caractere, this.fluxoDeCaractere.getLinha(), this.fluxoDeCaractere.getColuna());
				}

				if (caractere == '\'') {
					contAspasSimples--;
				}
				sb.append(caractere);
			}

			if (contAspasSimples > 0) {
				throw new AnalisadorLexicoUnknownRuntimeException(ErrosAnalisadorLexico.CADEIA_NAO_FECHADA,
						(Token) null);
			}

		}

		String lexema = sb.toString();

		if (lexema.equals("")) {
			return null;
		}

		token = new Token(lexema, "t_cadeia");

		if (token.getLexema().length() > TAM_MAXIMO_CADEIA + 2) {
			throw new AnalisadorLexicoUnknownRuntimeException(ErrosAnalisadorLexico.CADEIA_OUT_OF_RANGE, token,
					linha, coluna);
		}

		return token;
	}

	/**
	 * @param caractere
	 * @return True se for caractere início de Cadeia
	 */
	protected boolean ehInicioCadeia(Character caractere) {
		return caractere == '\'';
	}

	/**
	 * Observe que para o correto funcionamento o seguinte método deve ser chamado antes
	 * {@link AnalisadorLexico#ehInicioSinalDePontuacao(Character)}
	 * 
	 * @return O próximo sinal de pontuacao lido do fluxo de caracteres
	 */
	protected Token getSinalDePontuacao() {
		StringBuilder sb = new StringBuilder();
		Token token = null;
		Character caractere = null;

		if (this.fluxoDeCaractere.temProxCaractere()) {
			caractere = this.fluxoDeCaractere.getProxCaractere();
			sb.append(caractere);
		}

		String lexema = sb.toString();

		if (lexema.equals("")) {
			return null;
		}

		token = new Token(lexema, "t_sinal_pontuacao");

		return token;
	}

	/**
	 * @param caractere
	 * @return True se for caractere início de Sinal de Pontuação
	 */
	protected boolean ehInicioSinalDePontuacao(Character caractere) {
		return caractere == ';';
	}

	/**
	 * Observe que para o correto funcionamento o seguinte método deve ser chamado antes
	 * {@link AnalisadorLexico#ehInicioParentisador(Character)}
	 * 
	 * @return O próximo parentisador lido do fluxo de caracteres
	 */
	protected Token getParentisador() {
		StringBuilder sb = new StringBuilder();
		Token token = null;
		Character caractere = null;

		if (this.fluxoDeCaractere.temProxCaractere()) {
			caractere = this.fluxoDeCaractere.getProxCaractere();
			sb.append(caractere);
		}

		String lexema = sb.toString();

		if (lexema.equals("")) {
			return null;
		}

		token = new Token(lexema, "t_parentisador");

		return token;
	}

	/**
	 * @param caractere
	 * @return True se for caractere início de Parentisador
	 */
	protected boolean ehInicioParentisador(Character caractere) {
		return caractere == ')' || caractere == '(' || caractere == '[' || caractere == ']' || caractere == '{'
				|| caractere == '}';
	}

	/**
	 * Observe que para o correto funcionamento o seguinte método deve ser chamado antes
	 * {@link AnalisadorLexico#ehInicioOperador(Character)}
	 * 
	 * @return O próximo Operador do fluxo de caracteres.
	 * 
	 */
	protected Token getOperador() {
		StringBuilder sb = new StringBuilder();
		Token token = null;
		Character caractere = null;
		Character proxCaractere = null;

		if (this.fluxoDeCaractere.temProxCaractere()) {

			caractere = this.fluxoDeCaractere.getProxCaractere();

			if (!ehCaractereValido(caractere)) {
				throw new AnalisadorLexicoUnknownRuntimeException(ErrosAnalisadorLexico.SIMBOLO_INVALIDO,
						caractere, this.fluxoDeCaractere.getLinha(), this.fluxoDeCaractere.getColuna());
			}

			while (this.fluxoDeCaractere.temProxCaractere()
					&& !ehCaractereValido(proxCaractere = this.fluxoDeCaractere.getProxCaractere())) {
				throw new AnalisadorLexicoUnknownRuntimeException(ErrosAnalisadorLexico.SIMBOLO_INVALIDO,
						proxCaractere, this.fluxoDeCaractere.getLinha(), this.fluxoDeCaractere.getColuna());
			}

			sb.append(caractere);

			if (proxCaractere != null
					&& ((caractere == '>' && proxCaractere == '=') || (caractere == '<' && proxCaractere == '=')
							|| (caractere == '<' && proxCaractere == '>') || (caractere == ':' && proxCaractere == '='))) {
				sb.append(proxCaractere);
			} else if (proxCaractere != null) {
				this.fluxoDeCaractere.devolver(proxCaractere);
			}

		}

		String lexema = sb.toString();

		if (lexema.equals("")) {
			return null;
		}

		token = new Token(lexema, "t_operador");

		return token;
	}

	/**
	 * @param caractere
	 * @return True se for caractere início de Operador
	 */
	protected boolean ehInicioOperador(Character caractere) {
		return caractere == '+' || caractere == '-' || caractere == '*' || caractere == '/' || caractere == '='
				|| caractere == ':' || caractere == '>' || caractere == '<';
	}

	/**
	 * Observe que para o correto funcionamento o seguinte método deve ser chamado antes
	 * {@link AnalisadorLexico#ehInicioIdentificador(Character)}
	 * 
	 * @return O próximo Identificador lido do fluxo de caracteres
	 */
	protected Token getIdentificador() {
		StringBuilder sb = new StringBuilder();

		Character caractere = null;

		Token token = null;

		int linha = this.fluxoDeCaractere.getLinhaProxCaractereIgWS();
		int coluna = this.fluxoDeCaractere.getColunaProxCaractereIgWS();

		while (this.fluxoDeCaractere.temProxCaractere()) {
			caractere = this.fluxoDeCaractere.getProxCaractere();

			if (!ehCaractereValido(caractere)) {
				throw new AnalisadorLexicoUnknownRuntimeException(ErrosAnalisadorLexico.SIMBOLO_INVALIDO,
						caractere, this.fluxoDeCaractere.getLinha(), this.fluxoDeCaractere.getColuna());
			}

			if (!Character.isLetterOrDigit(caractere) && caractere != '_') {
				break;
			}
			sb.append(caractere);
			caractere = null;
		}

		if (caractere != null) {
			this.fluxoDeCaractere.devolver(caractere);
		}

		String lexema = sb.toString();

		if (lexema.equals("")) {
			return null;
		}

		token = new Token(lexema, "t_ident");

		if (token.getLexema().length() > TAM_MAXIMO_IDENTIFICADOR) {
			throw new AnalisadorLexicoUnknownRuntimeException(ErrosAnalisadorLexico.IDENTIFICADOR_OUT_OF_RANGE,
					token, linha, coluna);
		}

		return token;
	}

	/**
	 * @param caractere
	 * @return True se for caractere início de Identificador
	 */
	protected boolean ehInicioIdentificador(Character caractere) {
		return Character.isLetter(caractere) || caractere == '_';
	}

	/**
	 * Retorna o fluxo de caracteres desse analisador léxico.
	 * 
	 * @return o fluxo de caracteres desse analisador léxico
	 */
	public FluxoDeCaractere getFluxoDeCaractere() {
		return fluxoDeCaractere;
	}

}
